<?php
namespace TwinPeaks\Core;

const FRAMEWORK_DATATYPE_NS = 'TwinPeaks\Core\Datatype';
const FRAMEWORK_LIB_NS = 'TwinPeaks\Stdlib';
const FRAMEWORK_HTML_NS = 'TwinPeaks\Core\Html';

require __DIR__ . '/trait/TSingleton.php';
require __DIR__ . '/trait/TMultiton.php';
require __DIR__ . '/trait/TDecorator.php';
require __DIR__ . '/trait/TAppInjector.php';

require __DIR__ . '/CStdlib.php';

require __DIR__ . '/CDummy.php';
require __DIR__ . '/excep/CBaseException.php';
require __DIR__ . '/excep/CSysException.php';
require __DIR__ . '/excep/CCommonException.php';
require __DIR__ . '/Http/CUri.php';
require __DIR__ . '/Http/CRequest.php';
require __DIR__ . '/Http/CResponse.php';
require __DIR__ . '/CConfig.php';
require __DIR__ . '/common.php';

const ERR_SYSTEM  = 1;

abstract class CApplication
{	
	use TSingleton;
	
	const OPT_EXCEPTION_RE_THROW       = 1000;
	const OPT_SUPPRESS_OUTPUT          = 1001;
	const OPT_DISABLE_SYS_EXCEPTION    = 1002;

	const AUTOLOAD_PEAR_SCHEME = 1;

	/**
	 * @var
	 */
	protected $scriptFilename;
	
	/**
	 * @var array Namespace to filesystem directory map.
	 */
	private $autoloadMapping = [];
	
	/**
	 * @var array
	 */
	private $autoloadPrefix = [];
	
	/**
	 * @var array
	 */
	private $initArgs;
	
	/**
	 * @var array Array of path aliases
	 */
	private $pathAliases = [];
	
	/**
	 * @var array
	 */
	private $nodes = [];
	
	/**
	 * Must return framework root directory
	 * 
	 * @return string
	 */
	abstract function getFrameworkDirectory();
	
	/**
	 * Must return application root directory
	 * 
	 * @return string
	 */
	abstract function getAppDirectory();
	
	/**
	 * Must return application namespace
	 * 
	 * @return string
	 */
	abstract function getAppNamespace();
	
	
	/**
	 * @TODO Remove this method
	 * @return void
	 */
	function defineNodes() {  }
	
	
	protected $optionValues = [];
	
	/**
	 * @return CApplication
	 */
	final function setOption($id, $value = true)
	{
		$this->optionValues[$id] = $value;
		return $this;
	}
	
	/**
	 * @return $mixed
	 */
	final function getOption($id)
	{
		if (isset($this->optionValues[$id]))
			return $this->optionValues[$id];
		else
			return null;
	}
	
	
	/**
	 * @return void
	 */
	function _echo($str)
	{
		if ($this->getOption(self::OPT_SUPPRESS_OUTPUT))
			return;		
		echo $str;
	}
	
	/**
	 * @return void
	 */
	function _echof()
	{
		if ($this->getOption(self::OPT_SUPPRESS_OUTPUT))
			return;
		
		$args = func_get_args();
		$str = call_user_func_array('sprintf', $args);
		echo $str;
	}
	
	/**
	 * 
	 */
	protected function configure()
	{
		$this->config = new CConfig($this->getConfigFilename());
		$this->loadFile('%app%/main.conf.php');
	}

	/**
	 * Registers an array of namespaces
	 * 
	 * @param array $namespaces An array of namespaces (namespaces as keys and locations as values)
	 */
	function registerNamespaces(array $namespaces)
	{
		foreach ($namespaces as $namespace => $paths)
		{
			$this->autoloadMapping[$namespace] = (array) $paths;
		}
	}
	
	/**
	 * Registers a namespace.
	 *
	 * @param string $namespace The namespace
	 * @param array|string $paths The location(s) of the namespace
	 * @return void
	 */
	function registerNamespace($ns, $pathname)
	{
		$places = (array) $pathname;
		// Add trailing slash
		foreach ($places as $key => $value)
			$places[$key] = rtrim($value, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR;
			
		if (isset($this->autoloadMapping[$ns]))
			$this->autoloadMapping[$ns] = array_merge($this->autoloadMapping[$ns], $places);
		else
			$this->autoloadMapping[$ns] = $places;
	}
	
	/**
	 * Tries to load class defnition from different places
	 * 
	 * @return void
	 */
	final function loadClass($places)
	{
		$places = (array) $places;
		foreach ($places as $filename)
		{
			$realFilename = $this->resolvePath($filename);
			if (is_file($realFilename))
			{
				include_once $realFilename;
				return;
			}
		}
	}
	
	/**
	 * @return void
	 */
	final function loadClassByName($places, $className, $fileExt = '.php')
	{
		$places = (array) $places;
		foreach ($places as &$pathname)
		{
			$pathname = rtrim($pathname, '/');
			$pathname = $pathname . '/' . $className . $fileExt;
		}
		$this->loadClass($places);
	}
	
	/**
	 * @param $class Unqualified class name
	 * @access private
	 * @return integer
	 */
	private function getAutoloadScheme($class)
	{
		foreach ($this->autoloadPrefix as $prefix => $entry)
		{
			if (preg_match('#' . $prefix . '#i', $class))
				return $entry['scheme'];
		}
		return false;
	}
	
	/**
	 * @final
	 * @access protected
	 * @return void
	 */
	final protected function autoloadAddPrefix($prefix, $baseDir, $scheme)
	{
		$this->autoloadPrefix[$prefix] = [
			'scheme' => $scheme,
			'base-dir' => $baseDir
		];
	}
	
	/**
	 * @return void
	 */
	final protected function autoloadPearAddPrefix($prefix, $baseDir = null)
	{
		$this->autoloadAddPrefix($prefix, $baseDir, self::AUTOLOAD_PEAR_SCHEME);
	}
	
	private function findNamespaceBasedir($needle)
	{
		foreach ($this->autoloadMapping  as $namespace => $pathArray)
		{
			if ($namespace == $needle)
				return $pathArray;
		}		
		return null;
	}
	
	/**
	 * @return array
	 */
	protected function autoloadDefaultScheme($namespace, $unqClassName)
	{
		$classFilename = [];
		
		// Direct namespaces to base directories mapping.
		if (($basedirArray = $this->findNamespaceBasedir($namespace)) != null)
		{
			foreach ($basedirArray as $path)
			{
				$filename = $this->resolvePath($path);
				$filename .= $unqClassName . '.php';
				$classFilename[] = $filename;
			}
			return $classFilename;
		}
		
		$parts = explode('\\', $namespace);
		$topLevelNS = '';
		for ($i = 0, $n = count($parts); $i < $n; $i++)
		{
			$topLevelNS .= $i ? ('\\' . $parts[$i]) : $parts[$i];
			if (($basedirArray = $this->findNamespaceBasedir($topLevelNS)) != null)
			{
				$relativePath = join(DIRECTORY_SEPARATOR, array_slice($parts, $i + 1));
				foreach ($basedirArray as $path)
				{
					$filename = $this->resolvePath($path);
					$filename .= $relativePath . DIRECTORY_SEPARATOR;
					$filename .= $unqClassName . '.php';
					$classFilename[] = $filename;
				}
			}
		}
		
		return $classFilename;
	}
	
	/**
	 * @return array
	 */
	protected function autoloadPEARScheme($class, $fileExt = '.php')
	{
		// Although PHP class names are case-insensitive, many filesystems are not.
		// So be carefull.
		$filename = str_replace('_', '/', $class) . $fileExt;
		return [$filename];
	}
	
	/**
	 * @param string Fully qualified class name.
	 * @return array
	 */
	private function classNameToFilename($fqcn)
	{
		$namespace = null;
		
		$unq = getUnqualifiedClassName($fqcn, $namespace);
		$scheme = $this->getAutoloadScheme($unq);
		
		if ($scheme == false)
			return $this->autoloadDefaultScheme($namespace, $unq);
		elseif ($scheme == self::AUTOLOAD_PEAR_SCHEME)
			return $this->autoloadPEARScheme($unq);

		return false;
	}
	
	/**
	 * Register a class autoloader.
	 * 
	 * @param callback $callback A callback mapping fully qualified class name to
	 * a filename containing its definition.
	 * 
	 * @final
	 * @access protected
	 * @return void
	 */
	final protected function registerAutoloader($callback)
	{
		$autoloader = function($fqcn) use($callback)
		{
			$this->loadClass($callback($fqcn));
		};
		spl_autoload_register($autoloader, true);
	}
	
	/**
	 * Register default class autoloader.
	 * 
	 * @acess protected
	 * @retun void
	 */
	protected function bootstrap()
	{
		$this->definePathAlias('framework', $this->getFrameworkDirectory());
		$this->definePathAlias('F', $this->getFrameworkDirectory());
		$this->definePathAlias('application', $this->getAppDirectory());
		$this->definePathAlias('A', $this->getAppDirectory());
		$this->definePathAlias('Lib', '@F/lib');
		$this->definePathAlias('vendor', '@F/vendor');
		
		$this->registerAutoloader([$this, 'classNameToFilename']);

		$this->registerNamespace($this->getAppNamespace(), $this->getAppDirectory());
		$this->registerNamespace(__NAMESPACE__, ['@F/core/mvc/controller',
			'@F/core/mvc/model', '@F/core/mvc/view', '@F/lib']);
		
		$this->registerNamespace(FRAMEWORK_DATATYPE_NS, '@F/core/datatype');
		$this->registerNamespace(FRAMEWORK_HTML_NS, '@F/core/mvc/view/html');
		$this->registerNamespace(FRAMEWORK_LIB_NS, '@F/lib');
        
        register_shutdown_function([$this, 'onPHPFatalError']);
	}
    
    /**
     * @return vpod
     */
    function onPHPFatalError()
    {
        //var_dump(ob_get_clean());
        //var_dump(1);
    }
	
	/**
	 * @return void
	 */
	final function definePathAlias($alias, $path)
	{
		if (isset($this->pathAliases[$alias]))
			CSysException::pathAliasDefined($alias)->_throw(); // Cannot redefine path alias		
		$this->pathAliases[$alias] = $path;
	}
	
	/**
	 * @return boolean
	 */
	final function isPathAliasDefined($alias)
	{
		return isset($this->pathAliases[$alias]) ? true : false; 
	}
	
	/**
	 * Be sure of what you are going to do
	 * 
	 * @return void 
	 */
	final function redefinePathAlias($alias, $newPath)
	{
		$this->pathAliases[$alias] = $newPath;
	}

	/**
	 * @return string
	 */
	private function resolvePathHelper($match)
	{
		$alias = substr($match[0], 1);
		if (!isset($this->pathAliases[$alias]))
			CSysException::pathAliasNotDefined($alias)->_throw();			
		return $this->pathAliases[$alias];		
	}

	/**
	 * @return string
	 * @throws CException
	 */
	final function resolvePath($path)
	{
		while (strpos($path, '@') !== false)
			$path = preg_replace_callback('~@[^' . DIRECTORY_SEPARATOR . ']+~',
				[$this, 'resolvePathHelper'], $path);
		return $path;
	}
	
	/**
	 * @access protected
	 * @return void
	 */
	protected function onCreate()
	{
		//$this->initArgs = func_get_args();
		$this->bootstrap();	
	}
	
	/**
	 * @return string
	 */
	function getHostname()
	{
		return gethostname();
	}
	
    /**
     * Encoding conversion helper which wraps iconv and mbstring where they exist or throws
     * an exception where neither is available.
     *
     * @param string $string
     * @param string $to
     * @param array|string $from
     * @return string
     */
    function convertEncoding($string, $to, $from)
    {
		$result = '';
		if (function_exists('iconv'))
			$result = iconv($from, $to, $string);
		elseif (function_exists('mb_convert_encoding'))
			$result = mb_convert_encoding($string, $to, $from);
		else
			CException::encodingConversionFailed($string);

		if ($result === false)
			return ''; // return non-fatal blank string on encoding errors from users

		return $result;
    }
	
	/**
	 * 
	 */
	function warning()
	{
		
	}
	
	/**
	 * Returns filename of the main configuration file
	 * 
	 * @return string
	 */
	function getDefaultConfFilename()
	{
		return '@A/main.conf.php';
	}
	
	/**
	 * @return CConfig
	 */
	final function getConfig()
	{
		return CConfig::getInstance();
	}
		
	/**
	 * @return void 
	 */
	protected function setConfig()
	{
		$this->getConfig()->applyFromFile($this->getDefaultConfFilename());
	}

	/**
	 * @return object|null
	 */
	final static protected function findException($excep, $excepClassName)
	{
		while (($excep = $excep->getPrevious()) != null)
			if (get_class($excep) == $excepClassName)
				return $excep;

		return null;
	}
}
